#include <iostream>
#include <fstream>
#include <sstream>
#include "utils/PalabraAleatoria.h"
#include "utils/GeneradorAleatorio.h"
#include "utils/Normalizador.h"
#include "utils/FileSystem.h"
#include "Heap.h"
#include "Buffer.h"
#include "fusion/Merger.h"

#include "Test.h"


using namespace std;


// Devuelve el nombre del archivo binario que contiene los datos
string normalizar(){
	// Normaliza el diccionario
	Normalizador n;
	n.normalizar("utf8Texto.txt", "Diccionario.txt");

	string nomArchBin = "DiccionarioSalidaBinario";
	ifstream entrada("Diccionario.txt");
	ofstream salidaTexto("DiccionarioSalida.txt");
	ofstream salidaBinario(nomArchBin.c_str());

	char* termino = new char[PalabraAleatoria::TAM_PALABRA];
	long random;
	GeneradorAleatorio generador;
	const unsigned long LIMITE_GENERADOR= 1000000;

	// Guarda las palabras junto con su identificador aleatorio
	while (!entrada.eof()){
		entrada >> termino;

		if (!entrada.eof()){
			random = generador.generar(LIMITE_GENERADOR);
			PalabraAleatoria* pa= new PalabraAleatoria(termino, random);

			pa->guardar(&salidaBinario);
			pa->guardarComoTexto(&salidaTexto);

			delete pa;
		}
	}

	delete[] termino;

	entrada.close();
	salidaTexto.close();
	salidaBinario.close();

	return nomArchBin;
}//normalizar


// Ordena una particion de palabras aleatorias
void ordenarParticionTerminos(Heap<PalabraAleatoria>& heap, Buffer<PalabraAleatoria>& buffEntrada,
		Buffer<PalabraAleatoria>& buffSalida, Buffer<PalabraAleatoria>& buffCongelamiento);


// Vuelca el contenido del buffer a un archivo y lo deja vacio
void crearParticion(Buffer<PalabraAleatoria>& buff, int orden);


// Carga terminos desde el archivo al buffer
void cargar(Buffer<PalabraAleatoria>& buff, ifstream& fin);


// Toma un buffer desordenado, lo ordena y crea una sola particion
void descongelarBuffer(Buffer<PalabraAleatoria>& buff, int orden);


// Inicializa el heap desde el archivo de entrada
void initHeap(ifstream &fin, Heap<PalabraAleatoria> &heap);


//-------------------------------------------------------------------------------------

int main(int argc, char** argv) {

	cout << "Procesar diccionario MK-VI (v7)" << endl;

	cout << "\nPrimero se normaliza el diccionario... ";
	string nomArchBin = normalizar();
	cout << "listo!" << endl;

	cout << "\nSe ordena el archivo normalizado" << endl;

	// El buffer de lectura y el de escritura deben contener "n" elementos, el heap "10n"
	int tamBuffer = 100;
	Buffer<PalabraAleatoria> buffEntrada(tamBuffer);
	Buffer<PalabraAleatoria> buffSalida(tamBuffer);
	Buffer<PalabraAleatoria> buffCongelamiento(tamBuffer);
	Heap<PalabraAleatoria> heap(10 * tamBuffer);

	cout << "Buffers de " << tamBuffer << " elementos "
		<< "y heap de " << heap.getTamanioMaximo() << " elementos." << endl;

	// Abre el archivo e inicializa el heap y el buffer de entrada
	ifstream fin(nomArchBin.c_str(), ifstream::in | ifstream::binary);
	initHeap(fin, heap);
	cargar(buffEntrada, fin);

	// TODO: Chequear si el archivo esta vacio despues de cargar el heap
	// TODO: Chequear si el archivo esta vacio despues de cargar el buffer de entrada

	// La carpeta donde se guardaran las particiones
	FileSystem::createFolder("particiones");

	int ordenParticion = 0;
	while (!heap.estaVacio()){

		// Ordena una partición de terminos
		ordenarParticionTerminos(heap, buffEntrada, buffSalida, buffCongelamiento);

		// El Buffer de salida esta lleno, hay que vaciarlo
		if (buffSalida.getCantElem() == buffSalida.getCantMax()) {
			ordenParticion++;
//			Test::imprimirBuffer(buffSalida, "Buffer de salida lleno, se crea particion");
			crearParticion(buffSalida, ordenParticion);
		}

		// El buffer de entrada esta vacio, hay que llenarlo
		if (buffEntrada.getCantElem() == 0){
			cargar(buffEntrada, fin);
		}

		// El buffer de congelamiento esta lleno, hay que vaciarlo creando nuevas particiones
		if (buffCongelamiento.getCantElem() == buffCongelamiento.getCantMax()){
			ordenParticion++;
//			Test::imprimirBuffer(buffCongelamiento, "Se vacia el buffer de congelamiento");
			descongelarBuffer(buffCongelamiento, ordenParticion);
		}

		// Si el heap y el buffer de entrada estan vacios ya no hay elementos a ordenar
		if (heap.estaVacio() && !buffEntrada.getCantElem() == 0){

			// Recarga el heap con el buffer de entrada
			while (!heap.estaLleno() && buffEntrada.getCantElem() != 0){
				PalabraAleatoria p;
				buffEntrada.quitarElemento(p);
				heap.insertar(p);
			}

		}
	}

	fin.close();

	// Quedaron elementos en el buffer de salida
	if (buffSalida.getCantElem() != 0){
		ordenParticion++;
//		Test::imprimirBuffer(buffSalida, "Quedaron elementos en el buffer de salida");
		crearParticion(buffSalida, ordenParticion);
	}

	// Quedaron elementos en el buffer de congelamiento
	if (buffCongelamiento.getCantElem() != 0){
		ordenParticion++;
//		Test::imprimirBuffer(buffCongelamiento, "Quedaron elementos en el buffer de congelamiento");
		descongelarBuffer(buffCongelamiento, ordenParticion);
	}

//	Test::leerParticion(ordenParticion, "particiones");

	cout << "Ordenamiento completado."
		<< "\n-> Cantidad de particiones generadas: " << ordenParticion << endl;

	cout << "\nIndique la cantidad de archivos simultaneos para la fusion de resultado: ";
	int a = 2;
	cin >> a;

	// Comienza la fusion de los archivos
	Merger m(ordenParticion, "particiones");
	std::string nomArchFinal = m.fusionarParticiones(a);

//	Test::leerResultado(nomArchFinal);
	cout << "Fin." << endl;

	return 0;
}


//-------------------------------------------------------------------------------------


// Ordena las palabras hasta que se llene el buffer de salida o se vacie el de entrada
void ordenarParticionTerminos(Heap<PalabraAleatoria>& heap, Buffer<PalabraAleatoria>& buffEntrada,
		Buffer<PalabraAleatoria>& buffSalida, Buffer<PalabraAleatoria>& buffCongelamiento) {

	bool salidaLlena = false;
	bool entradaVacia = false;
	bool congeladorLleno = false;

	while (!heap.estaVacio() && !salidaLlena && !entradaVacia && !congeladorLleno){

		// Saca el primer termino del heap y lo guarda
		PalabraAleatoria termino;
		heap.remover(termino);

		// Evita un bug en el heap
		if (termino.getIdentificador() < PalabraAleatoria::MAXIMO_IMPOSIBLE()){
			buffSalida.agregarElem(termino);
		}

		// Evita problemas cuando el buffer de entrada esta vacio
		if (buffEntrada.getCantElem() >0){

			// Guarda una copia del termino actual para las comparaciones
			PalabraAleatoria terminoGuardado = termino;

			// Toma un termino del buffer de entrada
			buffEntrada.quitarElemento(termino);

			if (termino > terminoGuardado){
				// Se agrega el termino al heap
				heap.insertar(termino);

			} else {
				// Se congela el termino
				buffCongelamiento.agregarElem(termino);
			}
		}

		/* FIXME: Si el archivo esta vacio va a salir cada ciclo porque el buffer
		 * de entrada esta vacio y no puede llenarse.
		 * Funciona, pero es muy ineficiente y feo
		 */
		// Chequea el estado de los buffers
		if (buffEntrada.getCantElem() == 0) entradaVacia = true;
		if (buffSalida.getCantElem() == buffSalida.getCantMax()) salidaLlena = true;
		if (buffCongelamiento.getCantElem() == buffCongelamiento.getCantMax()) congeladorLleno = true;
	}
}


// Dado un buffer determinos lo guarda como un archivo binary
void crearParticion(Buffer<PalabraAleatoria>& buff, int orden){
	// Construye el nombre del archivo de particiones
	string nomArch = FileSystem::getNombreParticion(orden);
	nomArch = "particiones/" + nomArch;

	ofstream fout(nomArch.c_str(), ofstream::out | ofstream::binary | ofstream::trunc);

	while (buff.getCantElem() != 0){
		PalabraAleatoria p;
		buff.quitarElemento(p);

		p.guardar(&fout);
	}

	fout.close();
}


// Carga en el buffer todos los terminos que entran desde el archivo
void cargar(Buffer<PalabraAleatoria>& buff, ifstream& fin){

	while (!fin.eof() && buff.getCantElem() < buff.getCantMax()){
		// Carga una palabra desde el archivo
		PalabraAleatoria termino;
		termino.cargarPalabra(fin);

		buff.agregarElem(termino);
	}
}


// Ordena el buffer mediante un heap y lo usa para crear una particion
void descongelarBuffer(Buffer<PalabraAleatoria>& buff, int orden){
	// Un heap temporal del mismo tamaño que el buffer
	Heap<PalabraAleatoria> hTemp(buff.getCantElem());

	// Carga el heap
	while (!hTemp.estaLleno()){
		PalabraAleatoria p;
		buff.quitarElemento(p);
		hTemp.insertar(p);
	}

	// Mueve los elementos ordenados al buffer
	while (!hTemp.estaVacio()){
		PalabraAleatoria p;
		hTemp.remover(p);
		buff.agregarElem(p);
	}

	// Guarda todo el buffer ordenado en su propia particion
	crearParticion(buff, orden);
}


// Inicializa el heap con los datos del archivo
void initHeap(ifstream &fin, Heap<PalabraAleatoria> &heap){

	while (!fin.eof() && !heap.estaLleno()){

		// Carga una palabra desde el archivo
		PalabraAleatoria termino;
		termino.cargarPalabra(fin);

		// Con cada inserción se hace un ordenamiento
		if (!fin.eof()) heap.insertar(termino);
	}
}
